﻿using System;
using System.Reflection;

namespace Algoritmia.Utils.Numerics
{
    public class BuiltinNumericsArithmeticsProvider<T> : IArithmeticsProvider<T>
        where T : IComparable<T>
    {
        private readonly Func<T, T, object> _addition;
        private readonly Func<T, T, object> _substraction;
        private readonly Func<T, T, object> _multiplication; 
        private readonly Func<T, T, object> _divide;

        private readonly T _zero;
        private readonly T _minValue;
        private readonly T _maxValue;
        private readonly T _one;

        public BuiltinNumericsArithmeticsProvider()
        {
            _addition = GetAddition();
            if ( _addition == null)
            {
                throw new ArgumentException("the type provided to the class is not a built-in numeric type.");
            }
            _substraction = GetSubstraction();
            _multiplication = GetMultiplication();
            _divide = GetDivision();
            _zero = default(T);
            _one = GetOne();
            object r = GetMinimumValue();
            if ( r == null)
            {
                throw new ArgumentException("the type provided to the class is not built-in a numeric type.");
            }
            _minValue = (T) r;
            r = GetMaximumValue();
            if (r == null)
            {
                throw new ArgumentException("the type provided to the class is not a built-in numeric type.");
            }
            _maxValue = (T) r;
        }

        private static object GetMinimumValue()
        {
            var op =  typeof(T).GetField("NegativeInfinity", BindingFlags.Static | BindingFlags.Public);
            op = op ?? typeof(T).GetField("MinValue", BindingFlags.Public | BindingFlags.Static);
            return op == null? null : op.GetValue(null);
        }

        private static object GetMaximumValue()
        {
            var op = typeof(T).GetField("PositiveInfinity", BindingFlags.Public | BindingFlags.Static);
            op = op ?? typeof(T).GetField("MaxValue", BindingFlags.Static | BindingFlags.Public);
            return op == null ? null : op.GetValue(null);
        }

        #region Private Resolving Arithmetics methods

        private static T GetOne()
        {
            var type = typeof(T);

            if (type == typeof(short))
            {
                return (T) (object) (short)1;
            }
            if (type == typeof(byte))
            {
                return (T) (object) (byte) 1;
            }
            if (type == typeof(int))
            {
                return (T) (object) 1;
            }
            if (type == typeof(long))
            {
                return (T) (object) (long) 1;
            }
            if (type == typeof(ushort))
            {
                return (T)(object)(ushort)1;
            }
            if (type == typeof(uint))
            {
                return (T)(object)(uint)1;
            }
            if (type == typeof(ulong))
            {
                return (T)(object)(ulong)1;
            }
            if (type == typeof(float))
            {
                return (T)(object)1.0f;
            }
            if (type == typeof(double))
            {
                return (T)(object)1.0;
            }
            if (type == typeof(decimal))
            {
                return (T) (object) 1.0M;
            }
            return default(T);
        }

        private static Func<T, T, object> GetAddition()
        {
            var type = typeof (T);
            if ( type == typeof(short) )
            {
                return (a, b) => (short) (object) a + (short) (object) b;
            }
            if (type == typeof(byte))
            {
                return (a, b) => (byte)(object)a + (byte)(object)b;
            }
            if (type == typeof(int))
            {
                return (a, b) => (int)(object)a + (int)(object)b;
            } 
            if (type == typeof(long))
            {
                return (a, b) => (long)(object)a + (long)(object)b;
            }
            if (type == typeof(ushort))
            {
                return (a, b) => (ushort)(object)a + (ushort)(object)b;
            }
            if (type == typeof(uint))
            {
                return (a, b) => (uint)(object)a + (uint)(object)b;
            }
            if (type == typeof(ulong))
            {
                return (a, b) => (ulong)(object)a + (ulong)(object)b;
            }
            if (type == typeof(float))
            {
                return (a, b) => (float)(object)a + (float)(object)b;
            }
            if (type == typeof(double))
            {
                return (a, b) => (double)(object)a + (double)(object)b;
            }
            if (type == typeof(decimal))
            {
                return (a, b) => (decimal)(object)a + (decimal)(object)b;
            }
            return null;
        }

        private static Func<T, T, object> GetSubstraction()
        {
            var type = typeof(T);
            if (type == typeof(short))
            {
                return (a, b) => (short)(object)a - (short)(object)b;
            }
            if (type == typeof(byte))
            {
                return (a, b) => (byte)(object)a - (byte)(object)b;
            }
            if (type == typeof(int))
            {
                return (a, b) => (int)(object)a - (int)(object)b;
            }
            if (type == typeof(long))
            {
                return (a, b) => (long)(object)a - (long)(object)b;
            }
            if (type == typeof(ushort))
            {
                return (a, b) => (ushort)(object)a - (ushort)(object)b;
            }
            if (type == typeof(uint))
            {
                return (a, b) => (uint)(object)a - (uint)(object)b;
            }
            if (type == typeof(ulong))
            {
                return (a, b) => (ulong)(object)a - (ulong)(object)b;
            }
            if (type == typeof(float))
            {
                return (a, b) => (float)(object)a - (float)(object)b;
            }
            if (type == typeof(double))
            {
                return (a, b) => (double)(object)a - (double)(object)b;
            }
            if (type == typeof(decimal))
            {
                return (a, b) => (decimal)(object)a - (decimal)(object)b;
            }
            return null;
        }

        private static Func<T, T, object> GetMultiplication()
        {
            var type = typeof(T);
            if (type == typeof(short))
            {
                return (a, b) => (short)(object)a * (short)(object)b;
            }
            if (type == typeof(byte))
            {
                return (a, b) => (byte)(object)a * (byte)(object)b;
            }
            if (type == typeof(int))
            {
                return (a, b) => (int)(object)a * (int)(object)b;
            }
            if (type == typeof(long))
            {
                return (a, b) => (long)(object)a * (long)(object)b;
            }
            if (type == typeof(ushort))
            {
                return (a, b) => (ushort)(object)a * (ushort)(object)b;
            }
            if (type == typeof(uint))
            {
                return (a, b) => (uint)(object)a * (uint)(object)b;
            }
            if (type == typeof(ulong))
            {
                return (a, b) => (ulong)(object)a * (ulong)(object)b;
            }
            if (type == typeof(float))
            {
                return (a, b) => (float)(object)a * (float)(object)b;
            }
            if (type == typeof(double))
            {
                return (a, b) => (double)(object)a * (double)(object)b;
            }
            if (type == typeof(decimal))
            {
                return (a, b) => (decimal)(object)a * (decimal)(object)b;
            }
            return null;
        }

        private static Func<T, T, object> GetDivision()
        {
            var type = typeof(T);
            if (type == typeof(short))
            {
                return (a, b) => (short)(object)a / (short)(object)b;
            }
            if (type == typeof(byte))
            {
                return (a, b) => (byte)(object)a / (byte)(object)b;
            }
            if (type == typeof(int))
            {
                return (a, b) => (int)(object)a / (int)(object)b;
            }
            if (type == typeof(long))
            {
                return (a, b) => (long)(object)a / (long)(object)b;
            }
            if (type == typeof(ushort))
            {
                return (a, b) => (ushort)(object)a / (ushort)(object)b;
            }
            if (type == typeof(uint))
            {
                return (a, b) => (uint)(object)a / (uint)(object)b;
            }
            if (type == typeof(ulong))
            {
                return (a, b) => (ulong)(object)a / (ulong)(object)b;
            }
            if (type == typeof(float))
            {
                return (a, b) => (float)(object)a / (float)(object)b;
            }
            if (type == typeof(double))
            {
                return (a, b) => (double)(object)a / (double)(object)b;
            }
            if (type == typeof(decimal))
            {
                return (a, b) => (decimal)(object)a / (decimal)(object)b;
            }
            return null;
        }

        #endregion

        #region Implementation of IArithmeticsProvider<in T>

        public T MinValue
        {
            get { return _minValue; }
        }

        public T MaxValue
        {
            get { return _maxValue; }
        }

        public T Zero
        {
            get { return _zero; }
        }

        public T One
        {
            get { return _one; }
        }

        public object Add(T a, T b)
        {
            return _addition(a, b);
        }

        public object Substract(T a, T b)
        {
            return _substraction(a, b);
        }

        public object Multiply(T a, T b)
        {
            return _multiplication(a, b);
        }

        public object Divide(T a, T b)
        {
            return _divide(a, b);
        }

        public T Min(T a, T b)
        {
            if ( a.CompareTo(b) < 0)
            {
                return a;
            }
            return b;
        }

        public T Max(T a, T b)
        {
            if ( a.CompareTo(b) > 0)
            {
                return a;
            }
            return b;
        }

        #endregion
    }
}
